延續上一節的內容,這次換成製作開關抽屜的功能,在認識知識點之前,先來完成一個簡單的抽屜。
抽屜基本架構分成在外面的標題 title 、打開抽屜後顯示的內容 content ,以及控制內容是否被打開的布林值 showContent。我們可以將這些資料包成物件裝進陣列中,再用 map 渲染。
標題用 TouchOpacity 來製作,點擊時觸發函式,使 drawerDataList 裡對應的 showContent 被更新。注意,因為 React 更新方式,我們必須使用產生新值的方式,因此必須用 map 來替換掉要更新的值。最後再加上一點樣式,一個最基本的抽屜就完成了。
function Drawer() {
const [drawerDataList, setDrawerDataList] = useState([
{
title: 'A',
content:
'content Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent Acontent A',
showContent: false,
},
{
title: 'B',
content:
'content Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent Bcontent B',
showContent: false,
},
{
title: 'C',
content:
'content Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent Ccontent C',
showContent: false,
},
]);
const handlePress = title => {
const newDrawerDataList = drawerDataList.map(drawerData => {
if (drawerData.title === title) {
return {
...drawerData,
showContent: !drawerData.showContent,
};
}
return drawerData;
});
setDrawerDataList(newDrawerDataList);
};
return (
<View>
{drawerDataList.map(drawerData => (
<View key={drawerData.title}>
<TouchableOpacity
onPress={() => handlePress(drawerData.title)}
style={styles.drawerTitle}>
<Text>{drawerData.title}</Text>
</TouchableOpacity>
{drawerData.showContent && (
<View style={styles.drawerContainer}>
<Text>{drawerData.content}</Text>
</View>
)}
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
drawerTitle: {
padding: 10,
backgroundColor: '#888',
borderBottomWidth: 1,
},
drawerContainer: {
paddingHorizontal: 8,
paddingVertical: 10,
},
});
接著我們可以把這個元件在任何需要的地方引入,也可以依照需求改寫成由外部傳入抽屜資料陣列。不過可能會遇到一個問題,當抽屜上方的內容很多,導致抽屜本身剛好在畫面最下方時,即使點擊了抽屜,抽屜也正常運作,使用者也可能因為看不到抽屜打開誤以為 App 壞了。其實只要手動下滑,就會發現抽屜是有開啟的。
function HomeScreen() {
return (
<ScrollView style={styles.container}>
<Text>
LoremLoremLoremLorem… 省略
</Text>
<Drawer />
</ScrollView>
);
const styles = StyleSheet.create({
container: {
height: 300,
},
});
因此就正式進入今天的主題,要如何讓抽屜元件打開後,可以自動下滑一點呢?首先要認識 onScroll 和 scrollEventThrottle 。
onScroll 這個事件顧名思義是在滑動螢幕時會被觸發,而要使用這個事件,必須搭配 scrollEventThrottle={16}
。因為我們是希望整個畫面下滑,因此將這些相關設定,設在外部元件中:
function HomeScreen() {
const scrollRef = useRef();
const handleScroll = () => {
console.log('scroll');
};
return (
<ScrollView
ref={scrollRef}
scrollEventThrottle={16}
style={styles.container}
onScroll={handleScroll}>
<Text>
LoremLoremLoremLorem… 省略
</Text>
<Drawer />
</ScrollView>
);
}
因為我們會有很多不同的 drawerTitle ,需要取得點擊的那個一項的位置,才能根據該位置,設定 scrollTo 來下滑。而在 onScroll 的事件裡,只要點擊不同的位置,就能取得不同的 e 值。我們可以在 e.nativeEvent 下找到 contentOffset ,也就是我們所需要的螢幕位置,並且把它儲存到 screenPosition 中。
function HomeScreen() {
const [screenPosition, setScreenPosition] = useState(0);
const scrollRef = useRef();
const handleScroll = e => {
const {contentOffset} = e.nativeEvent;
const {y} = contentOffset;
setScreenPosition(y);
};
}
有了正確的螢幕位置,就能進一步將此螢幕位置的 y 軸加一點點數字,達到下滑的目的。運用上一節學到的 useRef 和 scrollTo 可以建立一個處理下滑的 scrollDownDrawer 函式。
function HomeScreen() {
const scrollDownDrawer = () => {
scrollRef.current?.scrollTo({
y: screenPosition + 100,
animated: true,
});
};
}
不過,該在什麼時候觸發 scrollDownDrawer 呢?會這麼說是因為,抽屜本來是閉合的,打開後螢幕大小會跟著改變,單純用 scrollTo 似乎無法抵達那些新增區塊的高度。
還好 ScrollView 提供我們一個 props 叫 onContentSizeChange 。當螢幕大小改變時,就會觸發裡頭的內容。我們可以透過他,來呼叫 scrollTo 。
function HomeScreen() {
return (
<ScrollView
ref={scrollRef}
onContentSizeChange={scrollDownDrawer} // 新增這行
scrollEventThrottle={16}
style={styles.container}
onScroll={handleScroll}>
… 省略
)
}
這裡有一個 bug 。由於 onContentSizeChange 會在內容改變高度時觸發,因此剛開始載入頁面時,也會被觸發一次。因此應該改寫 scrollDownDrawer 為:
const scrollDownDrawer = () => {
if (screenPosition !== 0) {
// 避免一進入畫面,就直接往下滑動
scrollRef.current?.scrollTo({
y: screenPosition + 100,
animated: true,
});
}
};
不過這樣又有另一個問題,就是當使用者完全沒滑動頁面,就直接點擊抽屜時,是無法觸發 scrollDownDrawer 的。對此還有另一種暴力解法,將 screenPosition 和 setScreenPosition 傳到 Drawer 元件中。當點擊標題時若 screenPosition 是 0 ,代表沒有滑動過,則強制把 screenPosition 設為 10 ,這樣 scrollDownDrawer 就能正常運作了。
function HomeScreen() {
return (
<Drawer
screenPosition={screenPosition}
setScreenPosition={setScreenPosition}
/>
}
function Drawer({screenPosition, setScreenPosition}) {
const handlePress = title => {
… 省略
if (screenPosition === 0) {
setScreenPosition(10);
}
};
}